developmentor - a developer services company				  
				      Essential .NET Security
   Modules

Web Services

Estimated time for completion: 60 minutes


Goals of this exercise:

  • Install the Web Service Enhancements (WSE) kit
  • Build WS-Security enabled clients and servers
  • Examine the resulting SOAP messages
  • Experiment with different authentication mechanisms
  • Experiment with message protection mechanisms

Overview

In this activity we'll play with an early implementation of WS-Security, the Web Service Enhacements toolkit from Microsoft. We'll use this to implement a simple authentication scheme, then add support for message integrity and confidentiality.


part 1- Installing WSE

In this part, you'll download and install WSE. This will get you started building WS-Security enabled web services.

steps:

  1. Surf to the MSDN Web Services site, and find the WSE download page (ask your instructor for help finding it).

  2. Download the MSI file and save it to your disk. Run this MSI file (it must run with Admin privileges) to install the WSE.


part 2- Writing and calling a simple Web Service

We need to have a web service and a client to work with, so in this part we build these components.

steps:

  1. Open up the server working directory for this lab and create a file called server.asmx using whatever text editor you like (Notepad works fine). Paste the following code into your file, and save it to disk:

    <%@webservice class='MyService' language='c#'%>
    using System;
    using System.Web;
    using System.Web.Services;
    
    [WebService(Namespace="http://www.develop.com/esec/sample")]
    public class MyService {
      [WebMethod]
      public string Ping(string input) {
        string result = "Input: " + input + "\n";
        return result;
      }
    }
    
  2. Run the IIS Admin tool (as an administrator) and add a virtual directory that points to the folder into which you just dropped your server.asmx file. Ask the instructor for help if you aren't familiar with how this is done.

  3. Open up the empty client project that has been provided for you, and add a web reference to the service you created in the last step. Ask the instructor for help if you aren't familiar with how this is done.

  4. Add the following code to your client application, then build and test to make sure that it works:

      MyService svc = new MyService();
    
      Console.WriteLine(svc.Ping("Hello world"));
    

    Note that you'll need to add a using statement for client.localhost in order for this code to compile.


part 3- Wiring up WSE and setting up a trace listener

Now that we have a basic web service running, we'll add support for the WSE and also set up a trace listener so we can watch the SOAP traffic between the client and server.

steps:

  1. In the client project, add a reference to the Microsoft.Web.Services assembly (note this is different from System.Web.Services!), which should show up in the standard list of .NET references in Visual Studio.

  2. Open up the Reference.cs file that was generated for you when you added the web reference. Expand the Web References node and drill down a bit. If the Reference.cs file (which contains the proxy class) is not visible, click the Show All Files icon in the Solution Explorer toolbar, and then expand the Reference.map node. You can do this in VS.NET by using File:OpenFile and drilling down into the "Web References" folder under your client project. Once you have this open, change the base class of the web service proxy (this is a class called "MyService") from System.Web.Services.Protocols.SoapHttpClientProtocol to Microsoft.Web.Services.WebServicesClientProtocol. This is somewhat kludgy, but it's required by WSE at the moment. This base class allows the client to control WSE features, as you'll see shortly.

    While in Reference.cs, also change the URL from http://localhost/... to http://localhost:8080/... so you can watch the traffic using the soap trace utility. Start a new formatted trace using this tool now.

  3. Without making any other changes, try recompiling your client and call the server. What happens?

    Well, if you forgot to start your trace listener, you won't be able to connect to the service, so get that started or ask the instructor for help. Assuming you've got that sorted out, you should be seeing an exception, because the client is sending new headers (actually WSE is doing this) and the server doesn't understand them. You can see this in the trace output - at least one header is marked MustUnderstand.

  4. We need to wire up the server to WSE so it knows about these new headers. To do this, add a web.config file in your server directory with the following contents:

    <configuration>
      <system.web>
        <webServices>
          <soapExtensionTypes>
             <!-- add reference to WebServicesExtension,
                  note that type name is wrapped for readability,
                  real type name MUST NOT contain newlines -->
             <add type="Microsoft.Web.Services.WebServicesExtension,
                        Microsoft.Web.Services,Version=1.0.0.0,
                        Culture=neutral,PublicKeyToken=31bf3856ad364e35"
              priority="1" group="0"/>
          </soapExtensionTypes>
        </webServices>
      </system.web>
    </configuration>
    
  5. Now try running your client again, and it should work. WSE is now being called by the ASP.NET web service framework, and it's stripping out these headers and turning them into objects that you can use programmatically (we'll do this shortly).


part 4- Sending a simple claim

In this example, we'll have the client add a Security header that includes a claim of identity.

steps:

  1. In your client code, add a using statement for Microsoft.Web.Services.Security.

  2. Create a new UsernameToken object, passing in any user name you like, some made up password, and PasswordOption.SendNone to indicate that we'd rather not send the password (even digested) to the server.

  3. Get a reference to the Security object (this represents the WS-Security header element) and add your token to the header with the following code:

    Security secHeader = svc.RequestSoapContext.Security;
    secHeader.Tokens.Add(token);
  4. Whenever you use UsernameTokens with WSE, a password provider is required on the server in order to allow WSE to validate passwords and obtain the required keying material to validate MACs and decrypt messages. A simple implementation has been provided for you here. Open this project, have a look at the MyPasswordProvider.cs file, then build and copy the resulting assembly into the bin subdirectory for your server.

    Now you need to tell WSE where to find your password provider, so in your server's web.config file, add the following block (under the <configuration> element:

      <configSections>
        <!-- add reference to config section, note that
             type name is wrapped for readability, real
             type name MUST NOT contain newlines -->
        <section name="microsoft.web.services"
               type="Microsoft.Web.Services.Configuration.
               WebServicesConfiguration,Microsoft.Web.Services,               
               Version=1.0.0.0, Culture=neutral,
               PublicKeyToken=31bf3856ad364e35"
        />
      </configSections>
      <!-- WSE config section -->
      <microsoft.web.services>
        <security>
          <passwordProvider type="MyPasswordProvider, MyWSEProviders" />
        </security>
      </microsoft.web.services>
    
  5. In your server, change the hardcoded return value from Ping to be something like this, which prints out the client's claim:

      public string Ping(string input) {
        string result = "Input: " + input + "\n";
        Security secHeader = HttpSoapContext.RequestContext.Security;
        foreach (SecurityToken secToken in secHeader.Tokens) {
          UsernameToken tok = secToken as UsernameToken;
          if (null != tok) {
            result += "User Name: " + tok.Username;
    
            // see if claim has acceptable proof-of-possession
            if (PasswordOption.SendHashed != tok.PasswordOption) {
                result += " (IDENTITY WAS NOT VERIFIED)";
            }
            result += "\n";
          }
        }
        return result;
      }
    

    In order for your new version of server.asmx to compile, you'll need to add an assembly reference and a couple using statements as well:

    <%@assembly name='Microsoft.Web.Services, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35'%>
    using Microsoft.Web.Services;
    using Microsoft.Web.Services.Security;
    
  6. Test your client and look at the resulting trace output. The client is making a claim here, but is providing no proof of that claim. The only way we'd believe this claim is if we trusted both the client and the network between the the client and server, which is very unlikely. In fact, you could set the password in the client's UsernameToken to anything, and the result would still be the same.

  7. In the client, change the PasswordOption from SendNone to SendHashed, rebuild, and test again. Note that the password is being checked now (try sending a password that doesn't agree with what the server's password provider says it should be). Look at the resulting trace to verify the password digest was sent.


part 5- Adding integrity protection

So far we've authenticated the client, but we've not provided any protection for the message itself. If a bad guy changes the message on the way from client to server, neither will be aware of it. Here we'll fix that by adding a signature (well, a message authentication code, really) to the message.

steps:

  1. In the client, add a Signature to the security header, using your UsernameToken as the signature key:

    secHeader.Elements.Add(new Signature(token));
    

    Build and run your client, and look at the resulting trace output. It should be considerably larger now. Exactly what parts of the message are signed? Use the trace output to find out.

  2. Let's have the server look for the client's signature. Add the following code to the end of the Ping function (right before the return statement):

        foreach (ISecurityElement secElem in secHeader.Elements) {
          Signature sig = secElem as Signature;
          if (null != sig && 0 != (SignatureOptions.IncludeSoapBody & sig.SignatureOptions)) {
            UsernameToken tok = sig.SecurityToken as UsernameToken;
            if (null != tok) {
              result += "The Body of this message was signed by: " + tok.Username;
            }
          }
        }
    

    Build and test your client. The server should now indicate that the signature is valid.

  3. Now that we're signing the message, sending the password digest is actually redundant, so flip the PasswordOption back to SendNone in the client. Then comment out the first foreach loop in Ping, as the client is now using a signature as proof-of-possession of the password. Build and test to make sure that the server still knows who signed the message.

  4. You can see what happens if a bad guy tries to forge the client's signature without knowing the password: as a test, provide a bad password in the client code and see what happens. Do you think Ping even gets called in this case?


part 6- Adding confidentiality

In this part, we'll use the client's password as keying material to encrypt the body of the message.

steps:

  1. At the time of this writing, UsernameToken only supports signatures. It does not support the following usage model (which you can get if you use X509SecurityToken):

    secHeader.Elements.Add(new EncryptedData(token));

    In order to use symmetrical encryption with WSE, you must supply a symmetric key on the client (and provide access to the same key on the server). How you exchange that key is your business - WSE doesn't help here.

  2. Here's some code to add to your client which generates a key based on a password. Paste the following methods into your client class:

        static EncryptionKey _generateKey(string userName, string password) {
            // convert the password into a 24 byte TripleDES key
            // (as of this writing, WSE only supports TripleDES
            // for symmetric encryption)
            byte[] keyMaterial = _passwordToTripleDESKeyMaterial(password);
            EncryptionKey key = new SymmetricEncryptionKey(TripleDES.Create(), keyMaterial);
    
            // instruct WSE to add a clause to the ds:KeyInfo element
            // that will tell the server key provider which user's password
            // needs to be retrieved in order to decrypt
            KeyInfoName kin = new KeyInfoName();
            kin.Value = userName;
            key.KeyInfo.AddClause(kin);
            
            return key;
        }
    
        static byte[] _passwordToTripleDESKeyMaterial(string password) {
            // encode the password into bytes using UTF8
            MemoryStream media = new MemoryStream();
            StreamWriter writer = new StreamWriter(media, new System.Text.UTF8Encoding());
            writer.Write(password);
            writer.Flush();
            byte[] encodedPassword = new Byte[media.Length];
            Array.Copy(media.GetBuffer(), 0, encodedPassword, 0, (int)media.Length);
            writer.Close();
    
            // we need 24 bytes of key material for a TripleDES key,
            // so hash the encoded password via SHA1 (this gives us 20 bytes)
            // the last four bytes of the key will be zero
            SHA1 hash = SHA1.Create();
            byte[] keyMaterial = new byte[24];
            Array.Copy(hash.ComputeHash(encodedPassword, 0, encodedPassword.Length), 0, keyMaterial, 0, hash.HashSize / 8);
            
            return keyMaterial;
        }
    

    To get the above code to compile, you'll need to add a reference to System.Security.dll (for XML encryption support), and a few using statements:

    using System.Security.Cryptography.Xml;
    using System.Security.Cryptography;
    using System.IO;
    
    // Since System.Security.Cryptography.Xml
    // also defines a class called "Signature",
    // This alias eliminates the ambiguity.
    // We want the WSE version.
    using Signature=Microsoft.Web.Services.Security.Signature;
    

    With this supporting code in place, you can now add the following line to your client in order to encrypt the message (replace userName and password with the name and password you're using):

            secHeader.Elements.Add(new EncryptedData(_generateKey(userName, password)));
    
  3. Build and test your client. It should fail with an exception from the WSE, which has absolutely no clue what key to use to decrypt the message at this point. We'll remedy this shortly, but take a look at the trace output anyway and notice that the content of the Body element has been replaced with an EncryptedData element.

  4. If you take another look at the WSE Provider project that you used earlier for your password provider, you'll see that there's also a decryption key provider implemented as well. Since you've already built this assembly and deployed it on your server, all you need to do is wire up the decryption key provider in your server's web.config file (place this right after the passwordProvider element):

          <decryptionKeyProvider type="MyDecryptionKeyProvider, MyWSEProviders" />
    

    Be sure to have a look at the provider to see what it's doing. Basically all it does is looks for a KeyInfoName clause in the encrypted data block to figure out whose password was used to encrypt the message, then it looks up that user's password (using the password provider in the same assembly) and converts the password into a TripleDES key, just like we did a moment ago in the client. It gives this key to WSE's HttpModule, who uses it to decrypt the message for us.

  5. Try running your client again. Things should work now - notice that the input you sent to the server has been successfully decrypted and echoed back to you.


part 7- Security issues in this sample

WSE (and the WS-Security spec it's built on) are very new, and will probably change during standardization. For now, here are some security issues in our code that are due to limitations in WSE and WS-Security:

steps:

  1. The server-side password provider deals in plaintext passwords. This is due to the WS-Security mechanism for authenticating passwords, and WSE's direct implementation of the spec. Here's a link to a thread discussing the problem and offering an idea for a solution.

  2. The symmetric encryption key is based on a long term secret (the password). We should really be using a short-term session key, but WSE currently doesn't support key encipherment with UsernameTokens. Things are a bit better if you use X.509 certificates with WSE.

Solutions: